package com.snailframework.gateway.route;

import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 * @author: zgadmin(zhangge)
 * @Date: 2023/2/23 上午10:41
 * @Version: 1.0.0
 */
@Slf4j
@Component
public class DynamicRoute implements InitializingBean {

    @Resource
    RouteDefinitionWriter routeDefinitionWriter;

    @Resource
    ApplicationEventPublisher applicationEventPublisher;

    @Resource
    RouteDefinitionLocator routeDefinitionLocator;

    @Resource
    NacosDiscoveryProperties nacosDiscoveryProperties;

    @Override
    public void afterPropertiesSet() throws Exception {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, nacosDiscoveryProperties.getServerAddr());
        properties.put(PropertyKeyConst.NAMESPACE, nacosDiscoveryProperties.getNamespace());
        properties.put(PropertyKeyConst.USERNAME, nacosDiscoveryProperties.getUsername());
        properties.put(PropertyKeyConst.PASSWORD, nacosDiscoveryProperties.getPassword());
        ConfigService configService = NacosFactory.createConfigService(properties);

        String routeConfig = configService.getConfig("snail-framework-gateway-router", "DEFAULT_GROUP_T1", 3000);

        log.info("==================== init route :{} ====================", routeConfig);

        List<RouteDefinition> routeDefinitions = initRoute(routeConfig);
        routeDefinitions.forEach(DynamicRoute.this::add);

        configService.addListener("snail-framework-gateway-router", "DEFAULT_GROUP_T1", new Listener() {
            @Override
            public Executor getExecutor() {
                return null;
            }

            @Override
            public void receiveConfigInfo(String routeConfig) {
                log.info("receive route config change:{}", routeConfig);
                clearBuffer();
                List<RouteDefinition> routeDefinitions = initRoute(routeConfig);
                routeDefinitions.forEach(DynamicRoute.this::modify);
                publish();
            }
        });
    }

    public List<RouteDefinition> initRoute(String routeConfig) {
        log.info("init route config:{}", routeConfig);
        try {
            List<RouteDefinition> routeDefinitions = JSONUtil.toList(routeConfig, RouteDefinition.class);
            if (null == routeDefinitions || routeDefinitions.isEmpty()) {
                return Collections.emptyList();
            }
            return routeDefinitions;
        } catch (Throwable throwable) {
            log.error("", throwable);
            log.error("init route config throwable:{}", routeConfig);
            return Collections.emptyList();
        }
    }

    public void clearBuffer() {
        List<RouteDefinition> routeDefinitions = routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
        log.info("clear route buffer :{}", JSONUtil.toJsonStr(routeDefinitions));
        if (null != routeDefinitions && !routeDefinitions.isEmpty()) {
            routeDefinitions.forEach(this::delete);
        }
    }

    public void add(RouteDefinition routeDefinition) {
        log.info("add route:{}", routeDefinition.getId());
        try {
            routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
        } catch (Throwable throwable) {
            log.error("", throwable);
            log.error("add route throwable, route id:{}", routeDefinition.getId());
        }
    }

    public void delete(RouteDefinition routeDefinition) {
        log.info("delete route:{}", routeDefinition.getId());
        try {
            routeDefinitionWriter.delete(Mono.just(routeDefinition.getId())).subscribe();
        } catch (Throwable throwable) {
            log.error("", throwable);
            log.error("delete route throwable, route id:{}", routeDefinition.getId());
        }
    }

    public void modify(RouteDefinition routeDefinition) {
        log.info("modify route:{}", routeDefinition.getId());
        try {
            routeDefinitionWriter.delete(Mono.just(routeDefinition.getId()));
        } catch (Throwable throwable) {
            log.error("", throwable);
            log.error("delete route throwable, route id:{}", routeDefinition.getId());
        }
        add(routeDefinition);
    }

    public void publish() {
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(routeDefinitionWriter));
    }

}